﻿// Jinx AJAX Framework
// Copyright (C) 2008 Blake Hegerle
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System.Collections;
using System.Collections.Generic;
using System.Text;
using Core.Parsing;
using Core.Parsing.ContextFree;

namespace Core.Json
{
    public class JsonLexer : IEnumerable<Token>
    {
        public static readonly Terminal<object> COLON;
        public static readonly Terminal<object> COMMA;
        public static readonly Terminal<bool> FALSE;
        public static readonly Terminal<double> FLOAT;
        public static readonly Terminal<long> INT;
        public static readonly Terminal<object> L_BRACE;
        public static readonly Terminal<object> L_BRACKET;
        public static readonly Terminal<object> NULL;
        public static readonly Terminal<object> R_BRACE;
        public static readonly Terminal<object> R_BRACKET;

        public static readonly Terminal<string> STRING;

        private static readonly IDictionary<char, Terminal<object>> Symbols;
        public static readonly Terminal<bool> TRUE;

        private readonly IEnumerator<char> Input;
        private bool EOF;

        static JsonLexer()
        {
            L_BRACE = new Terminal<object>("L_BRACE");
            R_BRACE = new Terminal<object>("R_BRACE");
            L_BRACKET = new Terminal<object>("L_BRACKET");
            R_BRACKET = new Terminal<object>("R_BRACKET");
            COMMA = new Terminal<object>("COMMA");
            COLON = new Terminal<object>("COLON");
            NULL = new Terminal<object>("NULL");

            TRUE = new Terminal<bool>("TRUE");
            FALSE = new Terminal<bool>("FALSE");
            STRING = new Terminal<string>("STRING");
            INT = new Terminal<long>("INT");
            FLOAT = new Terminal<double>("FLOAT");

            Symbols = new Dictionary<char, Terminal<object>>
                      {
                          {'{', L_BRACE},
                          {'}', R_BRACE},
                          {'[', L_BRACKET},
                          {']', R_BRACKET},
                          {',', COMMA},
                          {':', COLON},
                      };
        }

        public JsonLexer(IEnumerable<char> input)
        {
            Input = input.GetEnumerator();
        }

        private char c
        {
            get { return Input.Current; }
        }

        #region IEnumerable<Token> Members

        public IEnumerator<Token> GetEnumerator()
        {
            MoveNext();

            while (!EOF)
            {
                if (char.IsWhiteSpace(c))
                {
                    MoveNext();
                    continue;
                }

                if (char.IsDigit(c) || c == '-')
                    yield return EmitNumber();
                else
                {
                    switch (c)
                    {
                        case '"':
                            yield return EmitString();
                            break;
                        case 't':
                            yield return EmitTrueLiteral();
                            break;
                        case 'f':
                            yield return EmitFalseLiteral();
                            break;
                        case 'n':
                            yield return EmitNullLiteral();
                            break;
                        default:
                            yield return EmitSymbol();
                            break;
                    }
                }
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        private void MoveNext()
        {
            EOF = !Input.MoveNext();
        }

        private void Consume(string Expected)
        {
            var Next = ConsumeString((uint) Expected.Length);

            if (Next == null)
                throw new UnexpectedEndOfInput("literal " + Expected);

            if (Next != Expected)
                throw new IllegalLiteralValue(Expected);
        }

        private string ConsumeString(uint Num)
        {
            var Buffer = new StringBuilder();

            for (var i = 0u; i < Num; i++)
            {
                if (EOF)
                    return null;

                Buffer.Append(c);

                MoveNext();
            }

            return Buffer.ToString();
        }

        private Token EmitTrueLiteral()
        {
            Consume("true");

            return new Token(TRUE, true);
        }

        private Token EmitFalseLiteral()
        {
            Consume("false");

            return new Token(FALSE, false);
        }

        private Token EmitNullLiteral()
        {
            Consume("null");

            return new Token(NULL);
        }

        private Token EmitSymbol()
        {
            var Sym = c;

            Terminal<object> T;
            if (!Symbols.TryGetValue(Sym, out T))
                throw new UnexpectedCharacterOnInput(Sym);

            MoveNext();

            return new Token(T, Sym);
        }

        private Token EmitNumber()
        {
            var Buffer = new StringBuilder();

            var IsFloat = false;

            while (!EOF)
            {
                if (char.IsDigit(c) || c == '-')
                {
                    Buffer.Append(c);
                }
                else if (c == '.' || c == 'e' || c == 'E')
                {
                    IsFloat = true;
                    Buffer.Append(c);
                }
                else
                {
                    break;
                }

                MoveNext();
            }

            var Literal = Buffer.ToString();

            if (IsFloat)
            {
                double d;

                if (!double.TryParse(Literal, out d))
                    throw new IllegalLiteralValue(Literal);

                return new Token(FLOAT, d);
            }

            long x;

            if (!long.TryParse(Literal, out x))
                throw new IllegalLiteralValue(Literal);

            return new Token(INT, x);
        }

        private Token EmitString()
        {
            var Buffer = new StringBuilder();

            MoveNext();

            while (true)
            {
                if (EOF)
                    throw new UnexpectedEndOfInput("in string literal");

                if (c == '\\')
                {
                    Buffer.Append(ConsumeEscape());
                }
                else
                {
                    if (c == '"')
                        break;
                    Buffer.Append(c);
                    MoveNext();
                }
            }

            MoveNext();

            return new Token(STRING, Buffer.ToString());
        }

        private char ConsumeEscape()
        {
            MoveNext();

            if (EOF)
                throw new UnexpectedEndOfInput("character escape");

            var EscapeCode = c;

            MoveNext();

            switch (EscapeCode)
            {
                case '"':
                    return '"';
                case '\\':
                    return '\\';
                case '/':
                    return '/';
                case 'b':
                    return '\b';
                case 'f':
                    return '\f';
                case 'n':
                    return '\n';
                case 'r':
                    return '\r';
                case 't':
                    return '\t';
                case 'u':
                    break;
                default:
                    throw new BadCharacterEscape(EscapeCode.ToString());
            }

            // if we got here, it must be a unicode character escape

            var S = ConsumeString(4);

            if (S == null)
                throw new UnexpectedEndOfInput("in \\u escape");

            ushort CharCode;
            if (!ushort.TryParse(S, out CharCode))
                throw new BadCharacterEscape(S);

            return (char) CharCode;
        }
    }
}